#!/usr/bin/env python
#
# Created because I like PHP too much, and need
# a breath of fresh air from another programming
# language.
#
# tslocum@gmail.com
# http://www.tj9991.com
# http://www.kusaba.org

__author__ = 'Trevor Slocum'
__license__ = 'GNU GPL v3'

import cgi
import wsgiref.handlers
import Cookie
import os
import datetime
import re
import gettext

from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext import search
from google.appengine.ext.webapp import template
from time import strftime
from datetime import datetime
from hashlib import sha224

# Import PyIB's own modules
import datamodel
from usercontrol import *
from page import *
from img import *
from formatting import *
from settings import Settings

# Set to true if we want to have our webapp print stack traces, etc
_DEBUG = False

# Wishful thinking
gettext.bindtextdomain('pyib')
gettext.textdomain('pyib')
_ = gettext.gettext

# Reset the value to False, because of the app engine module caching system
Settings._ADMINISTRATOR_VIEW = False

class BaseRequestHandler(webapp.RequestHandler):
  """Supplies a common template generation function.

  When you call generate(), we augment the template variables supplied with
  the current user in the 'user' variable and the current webapp request
  in the 'request' variable.
  """
  def generate(self, template_name, template_values={}, doreturn=False):
    account_greeting = ''
    loggedin = False
    
    user = users.get_current_user()
    if user:
      loggedin = True
      account_greeting = 'Welcome, ' + user.nickname() + '. '
      url = users.create_logout_url(self.request.uri)
      url_linktext = _('Log out')
    else:
      account_greeting = 'You may post anonymously, or '
      url = users.create_login_url(self.request.uri)
      url_linktext = _('Log in')
      
    values = {
      'request': self.request,
      'user': users.GetCurrentUser(),
      'account_url_pretext': account_greeting,
      'account_url': url,
      'account_url_linktext': url_linktext,
      'loggedin': loggedin,
      'administrator': users.is_current_user_admin(),
      'administratorview': Settings._ADMINISTRATOR_VIEW,
      'title': 'PyIB',
      'application_name': 'PyIB',
      'is_page': 'false',
      'forced_anonymous': Settings.FORCED_ANONYMOUS,
      'disable_subject': Settings.DISABLE_SUBJECT,
      'MAX_FILE_SIZE': Settings.MAX_IMAGE_SIZE_BYTES,
      'maxsize_display': Settings.MAX_IMAGE_SIZE_DISPLAY,
      'maxdimensions': Settings.MAX_DIMENSION_FOR_OP_IMAGE,
    }
    values.update(template_values)
    
    directory = os.path.dirname(__file__)
    path = os.path.join(directory, os.path.join('templates', template_name))
    
    if doreturn:
      return template.render(path, values, debug=_DEBUG)
    else:
      self.response.out.write(template.render(path, values, debug=_DEBUG))

  """Simple self.redirect() replacement.  Using the command provided in the SDK yields ugly refreshes."""
  def redirect_meta(self, destination):
    self.response.out.write('<meta http-equiv="refresh" content="0;url=' + destination + '">--&gt; --&gt; --&gt;')
  
  def error(self, error):
    template_values = {
      'error': error,
      }
    self.generate('error.html', template_values)

class ViewImage(BaseRequestHandler):
  def get(self, viewtype, postkey, dummy_filename, dummy_filename_ext):
    post = datamodel.Post.get(postkey)
    if post:
      if viewtype == 'thumb':
        displayImage(self, post.image, post.image_mime, True)
      elif viewtype == 'cat':
        displayImage(self, post.image, post.image_mime, False, True)
      else:
        displayImage(self, post.image, post.image_mime)
    else:
      self.response.out.write('Could not find file %s.' % dummy_filename)
      
class FinishImage(BaseRequestHandler):
  def post(self):
    finish_id = self.request.POST['id']
    file_data = self.request.POST['file'].file.read()

    image = getImage(finish_id)
    if image:
      if not image.thumb_data:
        image.thumb_data = file_data
        image.put()

class FinishImageCat(BaseRequestHandler):
  def post(self):
    finish_id = self.request.POST['id']
    file_data = self.request.POST['file'].file.read()

    image = getImage(finish_id)
    if image:
      if not image.thumb_catalog_data:
        image.thumb_catalog_data = file_data
        image.put()
      
class MainPage(BaseRequestHandler):
  def get(self, pagenum=0):
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    pagenum = int(pagenum)
    if pagenum >= 0 and pagenum <= 9:
      fetchpage(self, 'front', pagenum)
    else:
      return self.error('Invalid page number.')

class ResPage(BaseRequestHandler):
  def get(self, thread, action=None):
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    thread = str(postIDToKey(thread))
    if not action:
      fetchpage(self, thread)
    else:
      threads = getposts(self, thread, 0, action)
      writepage(self, threads, thread)

class Catalog(BaseRequestHandler):
  def get(self):
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    page = datamodel.Page.all().filter('identifier = ', 'catalog').get()
    if not page:
      page = datamodel.Page(identifier='catalog')

      threads = datamodel.Post.all().filter('parentid = ', None).order('-bumped')
      posts = []
      rows = []
      
      i = 0
      for thread in threads:
        posts.append(thread)
        i += 1
        if i == 12:
          i = 0
          rows.append(posts)
          posts = []
      # If there are more posts which were not placed in their own row
      if posts != []:
        # Add them to another row
        rows.append(posts)
      
      template_values = {
        'rows': rows,
        'catalogdimensions': Settings.MAX_DIMENSION_FOR_IMAGE_CATALOG,
      }
      
      page.contents = self.generate('catalog.html', template_values, True)
      page.put()

    self.response.out.write(page.contents)

class Panel(BaseRequestHandler):
  def get(self):
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    template_values = {}
    
    if users.get_current_user():
      current_user = users.get_current_user()
      user_prefs = datamodel.UserPrefs().all().filter('user = ', current_user)
      user_prefs = user_prefs.get()
      template_values = {
        'user_prefs': user_prefs,
      }
    
    self.generate('panel.html', template_values)

class Search(BaseRequestHandler):
  def get(self):
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    results = None
    numresults = None
    query = self.request.get('query', '')
    
    if query != '':
      results = datamodel.Post.all().search(query)
      if results:
        numresults = results.count()
        if numresults == 0:
          numresults = None
                                 
    template_values = {
      'query': query,
      'results': results,
      'numresults': numresults,
    }
    
    self.generate('search.html', template_values)    

class Sitemap(BaseRequestHandler):
  def get(self):
    urls = []
    
    posts = datamodel.Post.all().filter('parentid = ', None).order('-bumped')
                                 
    template_values = {
      'posts': posts,
    }

    self.response.headers['Content-Type'] = 'text/xml'
    self.generate('sitemap.xml', template_values)
    
class Board(BaseRequestHandler):
  def post(self):
    parent_post = None
    is_noko = False
    
    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')
    
    request_parent = self.request.get('parent')
    if request_parent:
      post_ancestor = datamodel.Post.get(request_parent)

      # Make sure we are replying to a post starting a thread, not a reply
      if post_ancestor:
        if post_ancestor.parent is not None:
          parent_post = post_ancestor
        else:
          return self.error('Error: That post is a reply.')
      else:
        return self.error('Error: Unable to locate thread.')
        
    if parent_post:
       post = datamodel.Post(parent=parent_post)
       post.parentid = parent_post.postid
       post.posts = None
    else:
       post = datamodel.Post()
       post.parentid = None
       post.posts = 1
    
    post.name = cgi.escape(self.request.get('name')).strip()
    name_match = re.compile(r'(.*)#(.*)').match(post.name)
    if name_match:
      if name_match.group(2):
        post.name = name_match.group(1)
        post.tripcode = tripcode(name_match.group(2))
        
    post.email = cgi.escape(self.request.get('email')).replace("\"", '&quot;').strip()
    if Settings.DISABLE_SUBJECT:
      post.subject = ''
    else:
      post.subject = cgi.escape(self.request.get('subject')).strip()
    post.message = clickableURLs(cgi.escape(self.request.get('message')).rstrip()[0:8000])
    post.password = cgi.escape(self.request.get('password')).strip()

    # Set cookies for auto-fill
    cookie = Cookie.SimpleCookie(self.request.headers.get('Cookie'))
    cookie['pyib_name'] = self.request.get('name')

    if post.email.lower() == 'return' or post.email.lower() == 'noko':
      is_noko = True
      post.email = ''
    elif post.email.lower() != 'sage' and post.email.lower() != 'age':
      cookie['pyib_email'] = post.email
    
    cookie['pyib_password'] = post.password
    self.response.headers['Set-cookie'] = str(cookie)
    
    post.ip = str(self.request.remote_addr)
    if Settings.FORCED_ANONYMOUS or self.request.get('anonymous', None) is not None:
      post.anonymous = True
    else:
      post.anonymous = False

    if self.request.get('aa', None) is not None:
      post.aa = True

    thefile = self.request.params.get('file', None)

    image_data = None
    if thefile is not None:
      try:
        image_data = db.Blob(thefile.value)
      except:
        pass
      
    if image_data:
      image = datamodel.Image(data=image_data)
      imageinfo = getImageInfo(image_data)
      if imageinfo[0] in ('image/gif', 'image/png', 'image/jpeg'):
        if imageinfo[1] > 0 and imageinfo[2] > 0:
          is_not_duplicate = checkImageNotDuplicate(image_data)
          if is_not_duplicate[0]:
            post.image_original = cgi.escape(thefile.filename).strip()
            post.image_mime = imageinfo[0]
            
            post.image_size = len(image_data)
            post.image_size_formatted = str(long(post.image_size / 1024)) + 'KB'

            if post.image_size > Settings.MAX_IMAGE_SIZE_BYTES:
              return self.error('Error: That file is too large.')
            
            if post.image_mime == 'image/gif':
              post.image_extension = '.gif'
            else:
              if post.image_mime == 'image/png':
                post.image_extension = '.png'
              else:
                if post.image_mime == 'image/jpeg':
                  post.image_extension = '.jpg'
          
            post.image_width = imageinfo[1]
            post.image_height = imageinfo[2]
            
            if parent_post:
              maxsize = Settings.MAX_DIMENSION_FOR_REPLY_IMAGE
            else:
              maxsize = Settings.MAX_DIMENSION_FOR_OP_IMAGE

            # Calculate the dimensions for the thumbnail for /thumb/
            post.thumb_width, post.thumb_height = getThumbDimensions(post.image_width, post.image_height, maxsize)

            # Calculate the dimensions for the thumbnail for /cat/
            post.thumb_catalog_width, post.thumb_catalog_height = getThumbDimensions(post.image_width, post.image_height, Settings.MAX_DIMENSION_FOR_IMAGE_CATALOG)

            post.image_hex = getImageHex(image_data)
          else:
            return self.error('Error: That image has already been posted <a href="/res/' + str(is_not_duplicate[1]) + '.html#' + str(is_not_duplicate[2]) + '">here</a>.')
        else:
          return self.error('Error: Unable to read image dimensions.')
      else:
        return self.error('Error: Only GIF, JPG, and PNG files are supported.')
      try:
        image.put()
        post.image = str(image.key())
      except:
        return self.error('Error: Unable to store image.')
      
    if users.get_current_user():
      current_user = users.get_current_user()
      user_prefs = datamodel.UserPrefs().all().filter('user = ', current_user)
      user_prefs = user_prefs.get()
      if not user_prefs:
        user_prefs = datamodel.UserPrefs(user=current_user)
      
      post.author = current_user
      post.name = ''

    if not post.image:
      if not parent_post:
        return self.error('Error: Please upload an image to start a thread.')
      if post.message == '':
        return self.error('Error: Please input a message, and/or upload an image.')

    if not checkNotFlooding(self, (post.parentid is not None)):
      return self.error('Error: Flood detected.')
      
    if post.password:
      post.password = sha224(post.password).hexdigest()
    else:
      post.password = ''

    post.date_formatted = post.date.strftime("%y/%m/%d(%a)%H:%M:%S")
    post.deleted = False
    
    post.nameblock = nameBlock(post)

    if not parent_post:
      post.bumped = datetime.now()
    
    try:
      post.postid = Counter('Post_ID').inc()
      if parent_post:
        post.message = checkRefLinks(post.message, parent_post.postid)
      else:
        post.message = checkRefLinks(post.message, post.postid)
      post.message = checkQuotes(post.message)
      post.message = checkAllowedHTML(post.message)
      if not post.aa:
        post.message = post.message.replace("\n", '<br>')

      if post.image:
        queueThumbnails(post, image_data)

      post.put()
      if post.image:
        image.attachedpost = str(post.key())
        image.put()
    except Exception, message:
      if post.image:
        image.delete()
      return self.error('Error: Unable to store post in datastore.  Reason:\n' + str(message))

    if users.get_current_user():
      user_prefs.posts += 1
      if post.email.lower() == 'sage':
        user_prefs.posts_sage += 1
      user_prefs.put()
    
    if parent_post:
      parent_post.posts += 1
      if post.email.lower() != 'sage':
        parent_post.bumped = datetime.now()
      parent_post.put()
      
      threadupdated(self, request_parent)
    else:
      trimThreads(self)
      
      threadupdated(self, None)
    
    if is_noko:
      if parent_post:
        self.redirect_meta(threadURL(parent_post.key()))
      else:
        self.redirect_meta(threadURL(post.key()))
    else: 
      self.redirect_meta('/')

class Delete(BaseRequestHandler):
  def post(self):
    password = self.request.get('password', None)

    if not checkNotBanned(self, self.request.remote_addr):
      return self.error('You are banned.')

    if password != '':
      delete = self.request.get('delete', None)
      if delete:
        post = datamodel.Post.get(delete)

        if post:
          if post.password == sha224(password).hexdigest():
            imageonly = self.request.get('imageonly', None)
            
            if imageonly:
              if post.image:
                if post.parentid:
                  image = datamodel.Image.get(post.image)
                  if image:
                    image.delete()
                  post.image = None
                  post.image_hex = None
                  if post.message == '':
                    post.deleted = True
                  post.put()
                else:
                  return self.error('Images can not be deleted from the first post in threads.')
              else:
                return self.error('That post does not have an image attached to it.')
            else:
              deletePost(self, post)

            if post.parentid:
              threadupdated(self, post.parent().key())
              self.redirect_meta(threadURL(post.parent().key()))
            elif not imageonly:
              self.redirect_meta('/')
            else:
              threadupdated(self, post.key())
              self.redirect_meta(threadURL(post.key()))
          else:
            return self.error('Error: Incorrect password.')
        else:
          return self.error('Error: Please check a box next to a post.')
      else:
        return self.error('Error: Please check a box next to a post.')
    else:
      return self.error('Error: Please enter a password.')
    
class AdminPage(BaseRequestHandler):
  def get(self,arg1=None,arg2=None,arg3=None):
    page_text = ''
    
    if not users.is_current_user_admin():
      return self.error('You are not authorized to view this page.')

    Settings._ADMINISTRATOR_VIEW = True
    
    if arg1:
      if arg1 == 'delete':
        post = datamodel.Post.get(arg2)
        if post:
          deletePost(self, post)
          page_text += 'Post removed.'
        else:
          page_text += 'Error: Post not found.'
      elif arg1 == 'ban':
        post = datamodel.Post.get(arg2)
        if post:
          if checkNotBanned(self, post.ip):
            ban = datamodel.Ban()
            ban.ip = post.ip
            ban.placed = datetime.now()
            ban.put()
            page_text += 'Ban placed.'
          else:
            page_text += 'That user is already banned.'
        else:
          page_text += 'Error: Post not found.'
      elif arg1 == 'clearcache':
        pages = datamodel.Page.all()
        numpages = pages.count()
        pages = [page.delete() for page in pages]
        page_text += 'Page cache cleared: ' + str(numpages) + ' pages deleted.'
      elif arg1 == 'unusedimages':
        images = datamodel.Image.all().filter('atachedpost = ', None)
        numimages = images.count()
        images = [image.delete() for image in images]
        page_text += 'Unused images removed: ' + str(numimages) + ' images deleted.'
      elif arg1 == 'nameblocks':
        posts = datamodel.Post.all()
        # Because of the app engine's restrictions on response time, you will probably want to
        # change the above to something like below, incrementing the second argument every time
        # you run the nameblocks command, ensuring all posts are processed.  It's ugly, but it works.
        # posts = posts.fetch(50,0)
        for post in posts:
          post.nameblock = nameBlock(post)
          post.put()
        page_text += 'Nameblocks regenerated.'
    
    template_values = {
      'title': 'PyIB Management Panel',
      'page_text': page_text,
      'administrator': users.is_current_user_admin(),
    }
    
    self.generate('panel_admin.html', template_values)

# Counter code by vrypan.net
class Counter():
  """Unique counters for Google Datastore.
  Usage: c=Counter('hits').inc() will increase the counter 'hits' by 1 and return the new value.
  When your application is run for the first time, you should call the create(start_value) method."""
  def __init__(self, name):
    self.__name = name
    res = datamodel.Idx.all().filter('name = ', self.__name).fetch(1)
    #res = db.GqlQuery("SELECT * FROM Idx WHERE name = :1 LIMIT 1", self.__name).fetch(1)
    if (len(res)==0):
      self.__status = 0
    else:
      self.__status = 1
      self.__key = res[0].key()
  
  def create(self, start_value=0):
    """This method is NOT "thread safe". Even though some testing is done,
    the developer is responsible make sure it is only called once for each counter.
    This should not be a problem, since it sould only be used during application installation.
    """

    res = datamodel.Idx.all().filter('name = ', self.__name).fetch(1)
    #res = db.GqlQuery("SELECT * FROM Idx WHERE name = :1 LIMIT 1", self.__name).fetch(1)
    if (len(res)==0):
      C = datamodel.Idx(name=self.__name, count=start_value)
      self.__key = C.put()
      self.__status = 1
    else:
      raise ValueError, 'Counter: '+ self.__name +' already exists'
  
  def get(self):
    self.__check_sanity__()
    return db.get(self.__key).count
  
  def inc(self):
    self.__check_sanity__()
    db.run_in_transaction(self.__inc1__)
    return self.get()
  
  def __check_sanity__(self):
    if (self.__status==0):
      s = Counter('Post_ID').create()
      #raise ValueError, 'Counter: '+self.__name+' does not exist in Idx'
    else:
      pass
  
  def __inc1__(self):
    obj = db.get(self.__key)
    obj.count += 1
    obj.put()

def real_main():
  global _DEBUG, time_start
  time_start = datetime.now()
  
  application = webapp.WSGIApplication([('/', MainPage),
                                        (r'/([0-9]+).html', MainPage),
                                        (r'/res/(.*).html/(.*)', ResPage),
                                        (r'/res/(.*).html', ResPage),
                                        (r'/(cat|thumb|src)/(.*)/(.*)\.(gif|jpg|png)', ViewImage),
                                        ('/post', Board),
                                        ('/catalog.html', Catalog),
                                        ('/search', Search),
                                        ('/delete', Delete),
                                        ('/panel', Panel),
                                        (r'/finish_c', FinishImageCat),
                                        (r'/finish', FinishImage),
                                        ('/sitemap.xml', Sitemap),
                                        (r'/admin/(.*)/(.*)', AdminPage),
                                        (r'/admin/(.*)', AdminPage),
                                        ('/admin', AdminPage)],
                                        debug=_DEBUG)
  wsgiref.handlers.CGIHandler().run(application)

def profile_main():
  import cProfile, pstats
  
  prof = cProfile.Profile()
  prof = prof.runctx("real_main()", globals(), locals())
  print "<pre>"
  stats = pstats.Stats(prof)
  stats.sort_stats("time")  # Or cumulative
  stats.print_stats(500)
  # stats.print_callees()
  # stats.print_callers()
  print "</pre>"

if __name__ == "__main__":
  #profile_main()
  real_main()
